Skip to content

feat(v2): land agent-core-v2 engine and kap-server behind experimental flag#1441

Open
sailist wants to merge 612 commits into
mainfrom
kimi-code-v2
Open

feat(v2): land agent-core-v2 engine and kap-server behind experimental flag#1441
sailist wants to merge 612 commits into
mainfrom
kimi-code-v2

Conversation

@sailist

@sailist sailist commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

N/A — integration PR that lands the long-lived kimi-code-v2 branch; see Problem below.

Problem

The v2 line — the DI × Scope based agent-core-v2 engine, the kap-server that hosts it, and the minidb read model — has been built up on the long-lived kimi-code-v2 integration branch. This PR merges that branch into main.

The v2 engine and server are not the default runtime yet. Both the kimi -p prompt path and kimi server run route to v2 only when KIMI_CODE_EXPERIMENTAL_FLAG is set, and the v2 module graph is lazy-imported so it stays off the default path. With the flag unset, the CLI keeps the v1 engine and @moonshot-ai/server.

What changed

Scope: 1,207 files, +192,653 / −285. The bulk is three new packages; everything else is CLI wiring, e2e coverage, skills, and small v1 touch-ups.

1. agent-core-v2: the new DI × Scope engine (packages/agent-core-v2, +146k, 872 files)

  • New agent-core-v2 package: a DI scope engine with an explicit agent / session lifecycle.
  • Wire/op state engine with derived wire models and ReplayTimelineModel; IEventBus and Op.toEvent alongside wire.signal; context writes routed through ops and declared tool delivery.
  • Per-agent wire records (wire.jsonl); flattened agent hierarchy with swarm lifted to session.
  • Execution environment reorganized into separate filesystem / process / tool domains.
  • Goal mode: core goal workflow, budget enforcement, and per-turn goal injection.
  • Model layer: Model god-object and protocol domains; kosong vendored as the internal llmProtocol/kosong implementation (drops the @moonshot-ai/kosong and @moonshot-ai/kaos dependencies).
  • Built-in tools registered via DI / contributions, plugin management, session subagent host + Agent tool, shared rg locator/runner, AGENTS.md hierarchy loading.

2. kap-server: the v2 server (packages/kap-server, +26k, 139 files)

  • New @moonshot-ai/kap-server package — the Kimi Code server backed by agent-core-v2 (codenamed "server-v2").
  • Hosts agent-core-v2 sessions over REST (/api/v1, ~25 route modules) and WebSocket (/api/v1/ws with seq/epoch watermark and resync), and serves the web assets.
  • Auth and request-security hardening, /openapi.json via @fastify/swagger, and v2 session export (diagnostic zip archives).
  • v1 parity fixes carried along: broadcast interaction question / approval events, honor the before_id pagination cursor, treat loopback origins as same-origin for WS upgrade, and align MCP media output with v1.

3. minidb: embedded read model (packages/minidb, +13k, 77 files)

  • New @moonshot-ai/minidb package — a pure-Node.js embedded KV database (Redis-style in-memory KV with SQLite-style WAL/snapshot persistence, secondary indexes, TTL), used as the session-index read model.

4. CLI wiring (apps/kimi-code, +762, 19 files)

  • kimi -p and kimi server run select v2 vs. v1 via KIMI_CODE_EXPERIMENTAL_FLAG (lazy import); v1 stays the default.
  • Add the v2 harness adapters (cli/v2/*) and the prompt-session abstraction shared by both engines; update TUI adapters to handle v2 events.

5. Tooling, e2e and docs

  • packages/server-e2e: typed v2 ServerClient SDK with a drift test and e2e coverage.
  • .agents/skills: add the agent-core-dev and write-tests skills plus other skill updates; add the dependency-graph dev viewer.
  • GOAL.md: design doc for the goal-mode feature split.
  • Minor: packages/protocol, packages/acp-adapter, packages/agent-core, packages/server, packages/node-sdk, apps/kimi-web, the hash-imports build loaders, flake.nix, and changesets.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2975437

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@moonshot-ai/kimi-code Minor
@moonshot-ai/agent-core-v2 Minor
@moonshot-ai/protocol Minor
@moonshot-ai/kap-server Patch
@moonshot-ai/agent-core Patch
@moonshot-ai/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbb4a3a0c6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +239 to +240
const session = this.opts.core.accessor.get(ISessionLifecycleService).get(sessionId);
if (session === undefined) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resume cold sessions before accepting WS subscriptions

When a client reconnects to a session that exists only on disk, such as after a server-v2 restart, the default snapshot path can succeed via the disk reader without materializing the session in ISessionLifecycleService. The following /api/v1/ws subscribe then calls ensureState, but this get() only checks live sessions and returns undefined, so the subscribe ack reports not_found and the socket is never registered; if a later prompt request resumes the session, that client still receives no live turn events. Use resume() here or otherwise create a broadcaster state for cold persisted sessions before returning false.

Useful? React with 👍 / 👎.

Comment on lines +1011 to +1015
'file.not_found',
'file.too_large',
'fs.path_not_found',
'fs.permission_denied',
'validation.failed',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the Kimi error schema exhaustive

The KimiErrorCode union above includes additional codes such as prompt.not_found, session.busy, fs.path_escapes, fs.is_directory, fs.is_binary, fs.too_large, fs.already_exists, fs.too_many_results, fs.grep_timeout, and fs.git_unavailable, but this runtime z.enum omits them. Any error or turn.ended.error payload carrying one of those valid codes from agent-core-v2 will fail eventSchema / sessionEventMessageSchema validation even though the TypeScript type says it is valid, so add the missing literals or derive the schema from a single source.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

❌ Nix build failed

Hash mismatch in pnpmDeps:

Hash
specified sha256-iBk+TV+rIhmd7bYnVFbW3kTGltojJl3pL2hhmsGO+Fk=
got sha256-Z3daIqAm/BikwRSMXydiorikn5PMsxvWtB07SujJYzQ=

Please update flake.nix with the got hash.

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@9f1827e
npx https://pkg.pr.new/@moonshot-ai/kimi-code@9f1827e

commit: 9f1827e

sailist added 7 commits July 7, 2026 23:37
…mpty

server-v2 binds the main agent via profile:setModel without a cwd, so
the profile cwd is empty and planFilePathFor() produced a relative path
like plan/<id>.md. The plan-mode guard compares that against the Write
tool's workDir-resolved absolute path with strict equality, so it denied
every write to the plan file; the relative path also landed in
process.cwd() instead of the session workDir, breaking ExitPlanMode.

Make the plan path always absolute, falling back to the session workDir
(the same root the file tools resolve against) when the profile cwd is
empty.
Forward event.session.created to WS clients (it was published on the core
bus but never forwarded) and re-emit event.session.status_changed(running)
on turn.started. v2 derives session status via ISessionActivity, a pure pull
that publishes nothing, so these v1 broadcasts were dropped: clients that
did not issue a create never learned the session existed, and the running
transition never reached kimi-web, whose Stop button is gated on
session.status === 'running'. Mirrors v1's SessionService status emission
and isGlobalSessionEvent fan-out.
- add PromptHarness/PromptSession interfaces so the print driver is
  decoupled from the concrete SDK harness/session
- add an in-process agent-core-v2 harness (bootstrap + session +
  prompt/goal/permission adapters) under cli/v2/
- adapt v2 IEventBus events to the v1 Event union, with unit tests
- select the engine in createPromptHarness via isKimiV2Enabled()
  (KIMI_CODE_EXPERIMENTAL_FLAG), lazy-importing v2 off the default path
- remove unused imports and variables
- add void to floating promises
- drop redundant return-await
- bind unbound method references
- replace == null with strict null/undefined checks
- add explicit sort comparators and Array.from helpers
- rename background_tasks capability to tasks
- migrate event sink to event bus in tests
- add a per-connection outbound send buffer in WsConnectionV1; sendFrame
  enqueues and a flush drains on a 16ms interval or once 64 frames queue
- coalesce adjacent volatile assistant/thinking deltas for the same
  session/agent/turn into one frame, keeping the first offset/seq so
  client alignment is unchanged
- defer flushing while socket.bufferedAmount exceeds a 1MiB high-water
  mark, merging new deltas into the queued frame instead of growing it
- flush remaining frames on close to avoid truncating the tail of a stream
- expose flushIntervalMs / maxBatchSize / highWaterMarkBytes via registerWsV1
- bridge split v2 status slices into one combined `agent.status.updated`
  event via a LegacyStatus derived model at the kap-server edge, so a
  usage-only event no longer overwrites the live context window with a
  stale zero
- fall back to the configured default model's context window when the
  agent has no model bound yet, so fresh sessions don't show "0/0"
- use `size` (measured + estimated) for the live context token count to
  mirror v1's `context.tokenCount`
- export `defineDerivedModel` / `DerivedModelDef` from agent-core-v2
- add custom onSend/onResponse access logging that records the envelope code
- disable Fastify built-in request logging (every response is HTTP 200)
- drop pino-pretty; logger always emits newline-delimited JSON
- rename dev:server-v2 script to dev:kap-server
@sailist sailist changed the title feat(v2): land agent-core-v2 engine and server-v2 feat(v2): land agent-core-v2 engine and kap-server behind experimental flag Jul 8, 2026
sailist and others added 18 commits July 8, 2026 10:55
The v2 server package is @moonshot-ai/kap-server, but three changesets still referenced the old @moonshot-ai/server-v2 name, which no longer exists in the workspace.
Subagents created via the Agent tool or AgentSwarm fell back to the
default `manual` permission mode because CreateAgentOptions offered no
way to seed one, so write/execute tool calls inside a subagent prompted
for approval even when the parent ran in auto/yolo.

Add an optional permissionMode to CreateAgentOptions and have the Agent
tool and AgentSwarm pass the caller's current mode when creating a
child.
- add reduceContextTranscript in agent-core-v2: keeps the full history across
  context.apply_compaction (summary marker instead of dropping the prefix) and
  stops context.undo at compaction summaries, matching v1's transcript view
- SnapshotReader: reduce the wire with the transcript reducer instead of the
  folded live context, so a later undo no longer hides the pre-compaction
  assistant reply
- MessageLegacyService: read the main agent wire log and merge the unflushed
  live tail instead of the folded IAgentContextMemoryService.get()
- v1 session route handlers and the sessionLegacy adapter resolved the target
  via ISessionLifecycleService.get, which only sees live (in-memory) sessions.
  A freshly-opened session is persisted (index + disk) but not live until the
  first prompt, so commands such as undo / fs:* / skills / :btw / warnings /
  terminals / profile / questions / approvals / archive reported
  `session does not exist` (40401).
- Resolve via ISessionLifecycleService.resume instead, which cold-loads a
  persisted session from disk (matches v1's resumeSession); a genuinely
  missing session still 404s.
- Add a cold-load regression for `:undo` and update the skills test for the
  new cold-load behaviour.
- add an Agent-scope runtime domain that holds the whole live phase as one discriminated-union field (idle, running, streaming, tool_call, retrying, awaiting_approval, interrupted, ended)
- drive it from existing turn, step, delta, tool, retry, interrupt and approval events, edge-triggered with reference-equality dedup so delta bursts do not flood the wire log
- carry the phase on the existing agent.status.updated channel and add the AgentPhase type/schema to the protocol (backward compatible)
sailist and others added 30 commits July 10, 2026 14:50
- add media-owned `image` config section (`max_edge_px`, `read_byte_budget`)
  with `KIMI_IMAGE_MAX_EDGE_PX` / `KIMI_IMAGE_READ_BYTE_BUDGET` env bindings
  (env > config.toml > default)
- add Agent-scope `ImageConfigBridge` that pushes the env-resolved section
  into the image-compress resolver seam on load and on change, so all call
  sites honor config without per-call wiring
- resolve `maxEdge` / read-byte-budget defaults in image-compress via the new
  seam; keep the support module config-agnostic
- apply the read-image byte budget in ReadMediaFile's default downscale path
  (previously fell back to the 3.75 MB provider ceiling)
Reconcile the parallel goal-mode work on goal-parity with origin's newer
kimi-code-v2 architecture. Conflicts resolved by keeping origin's
mechanisms and layering parity's behavior on top:

- goalService: keep origin's activity-lease continuation launch; graft
  parity's settleGoalAfterContinuationFailure so a failed relaunch pauses
  the goal instead of stranding it. Drop the orphaned tokenUsageTotal
  helper (accounting already charges ctx.usage.output) and its unused
  TokenUsage import; remove a duplicate MAX_GOAL_COMPLETION_CRITERION_LENGTH.
- update-goal: layer parity's outcome-prompt tool results and no-goal
  guards onto origin's goalIsActive stopBatch refinement.
- promptService / fullCompactionService: dedupe imports; keep both DI
  params (toolSelect and instantiation are each used).
- tests: align continuation tests to the lease model (hold the turn lane
  and mock launchWithLease), swap abortController for signal, and keep
  parity's token-accounting assertions to match the merged code.
Port v1 #1508 into v2: lower the longest-edge downscale cap back to
2000px (v2 was stuck on the 3000px it had ported from an earlier v1
change) and make it overridable, add the 256 KB read-image byte budget
used by ReadMediaFile, and widen the over-budget fallback ladder to
[2000, 1000, 768, 512, 384, 256].

- MAX_IMAGE_EDGE_PX 3000 -> 2000, with KIMI_IMAGE_MAX_EDGE_PX env and a
  config-pushed value resolved via resolveMaxImageEdgePx.
- READ_IMAGE_BYTE_BUDGET=256KB with KIMI_IMAGE_READ_BYTE_BUDGET env and
  resolveReadImageByteBudget; ReadMediaFile's default compress path now
  uses it (region / full_resolution still honor IMAGE_BYTE_BUDGET).
- Test expectations that hard-coded 3000px / 1500px updated to 2000 /
  1000 to match the v1 behavior.

The [image] config.toml section is intentionally not added: the env
vars already cover the override path, and wiring a config section plus
a runtime push would add v2-specific scaffolding beyond v1.
…aFile

Align with v1: a HEIC/HEIF read is now refused up front with an
os-specific conversion command (sips / heif-convert / ImageMagick) so the
unsupported format never reaches the provider (which would reject the
whole session once it lands in history). The two guidance builders are
ported verbatim from v1, and the check sits after the image-capability
guard (using IHostEnvironment.osKind).

Also give the existing EXIF-rotation test a longer timeout: it does
heavy jimp encode/decode and was flaking around the default 5s boundary.
Align with v1: expose `startBtw` on AgentAPI and delegate it to the
existing ISessionBtwService (already implemented and DI-registered as
SessionBtwService), so the /btw slash command can fork a side-question
child agent once the server runs on v2.
- Remove resume-debug.test.ts: a one-off diagnosis script with a
  hardcoded local path, not a real test.
- Move streamTiming.test.ts to app/model/modelImpl.test.ts: it only
  exercises buildStreamTiming in app/model/modelImpl.ts.
- Rename wire/store.test.ts to wire/wireServiceImpl.test.ts to match the
  module it covers (there is no store.ts in src).
Align with v1: replace the hand-rolled subset validator with v1's Ajv-
based implementation (draft-07/2019/2020 + ajv-formats), so tool-call
argument validation once again honors the JSON Schema `format` keyword
(and the full keyword set), not just the previously hard-coded subset.

- args-validator.ts is now byte-identical to v1 (93 lines, replacing the
  289-line hand-rolled subset).
- Adds ajv@^8.18.0 and ajv-formats@^3.0.1 (same versions as v1) plus the
  pnpm-lock.yaml update.
- The two call sites (compileToolArgsValidator -> validateToolArgs) keep
  working unchanged; a small test locks in format / required /
  additionalProperties / subset behavior.
Align with v1: replace the hand-rolled subset validator with v1's Ajv-
based implementation (draft-07/2019/2020 + ajv-formats), so tool-call
argument validation once again honors the JSON Schema `format` keyword
(and the full keyword set), not just the previously hard-coded subset.

- args-validator.ts is now byte-identical to v1 (93 lines, replacing the
  289-line hand-rolled subset).
- Adds ajv@^8.18.0 and ajv-formats@^3.0.1 (same versions as v1) plus the
  pnpm-lock.yaml update.
- The two call sites (compileToolArgsValidator -> validateToolArgs) keep
  working unchanged; a small test locks in format / required /
  additionalProperties / subset behavior.
…ist tool source

Match the prompt change in 5cc8e52: the CronDelete parameter
description and invalid-id error now say "ULID" only (not
"ULID or legacy 8-hex"), and the CronList id doc comment likewise.
The validation regex is unchanged so loading any legacy 8-hex tasks
from disk still works.
Pull the 8 new upstream commits on top of the resolved goal-parity merge.
Reconciled where upstream now overlaps the parity work:

- update-goal: take origin's reimplementation (d9e411f) — no-goal
  fallbacks return a plain result, not a tool error. Dropped parity's
  contradicting "fail as a tool error" test; origin's goal-tools.test.ts
  covers the non-error behavior.
- promptService: keep parity's steer-buffer semantics (gap G24) — steers
  survive a cancelled/failed turn and flush into the next turn. Did not
  re-adopt origin's turn-result observation that discards them; kept
  parity's compaction-defer feature. Updated the retained test to the new
  LoopRunResult shape.
- goalService: keep the imports still used by the surviving goal-outcome
  code; drop TurnResult (no longer referenced after the upstream merge).
- goal.test.ts: adopt the reorganized test path (test/agent/goal) and its
  relative imports; keep the activity-lease import used by the
  continuation tests.
Fast catch-up merge; no conflicts. Brings in tool-args JSON Schema format
validation (adds ajv/ajv-formats deps), cron tool-source cleanup, and test
file tidy. No overlap with the goal-parity work.
It round-tripped restore over a hardcoded local dataset
(kimi-code-mini-bench/.vitest-results) that is not in the repo, so it
vacuously passed everywhere else. Removed at the original author's
request.
…test

The fuzz-style invariant test now drives the full v1 fallback ladder
([2000, 1000, 768, 512, 384, 256]) for over-budget inputs, which takes
longer than the default 5s boundary in this environment. Give it 30s.
- agent/task/manager.test.ts -> taskManager.test.ts (no manager.ts in
  agent/task; disambiguate from taskService.test.ts).
- app/cron/persist.test.ts -> cronTaskPersistenceService.test.ts (mirrors
  the module it covers; agent/task/persist.test.ts mirrors
  agent/task/persist.ts, so it is left as-is).
- agent/contextMemory/message.test.ts -> message-history.test.ts (its
  describe is 'message history (IAgentContextMemoryService)'; the dir
  already names the domain).
- sessionLifecycle: get/list no longer return a session whose cold resume is
  still in flight, so callers never observe a half-initialized handle; resume
  remains the way to await a fully restored handle
- messageLegacy: reduce the transcript from the main agent's in-memory wire
  journal instead of re-reading wire.jsonl; AgentWireRecordService now keeps
  the journal current with live dispatch so cold and live sessions both read a
  consistent, full transcript
micro-compaction only exists in legacy agent-core; v2 has no such mechanism. Update two comments that still cited it:

- fullCompactionService: the real reason not to project here is that llmRequester already projects once.

- swarmService: context.spliced consumers no longer include micro-compaction bookkeeping.
Since d5e1d76 every wire append rides the async persist queue whenever
a blob service is registered, but AgentWireRecordService.flush() only
awaited the log store. Callers - including the session-close path -
could complete a flush while records were still in flight on the queue,
and record-order assertions in tests raced it. Await the wire service
flush, which drains the persist queue, before flushing the log.
…ontinuation driver

The per-turn-boundary reminder test predates the goal continuation
driver and never passed: its second explicit prompt raced the
auto-launched continuation turn, which correctly holds the turn lane.
Treat the continuation turn as the second boundary and end it
deterministically by completing the goal through UpdateGoal, keeping
the once-per-boundary (never per-step) assertion.
…exhausted

Previously a goal whose hard budget was reached mid-turn was only
flipped to blocked while the turn kept running unbounded: the loop
continues unconditionally after a tool-calls step, and steer flushes or
Stop hooks could extend the turn indefinitely past the budget.

Now, when the over-budget step requested tool calls, a goal_budget_stop
system reminder is appended after the tool results telling the model the
goal is blocked (resumable via /goal resume), to stop immediately, that
further tool calls will be rejected, and to write a brief final status
message. The model gets exactly one grace step, during which tool calls
are answered with a soft rejection instead of executing. After the grace
step - or when the over-budget step had no pending tool calls - a new
AfterStepContext.stopTurn flag ends the turn, honored by the loop with
precedence over tool_calls and hook-set continue so nothing can extend
past the stop. The grace grant also respects maxStepsPerTurn so it can
never turn a budget stop into a max-steps turn failure.

Turn launch is gated as well: a prompt arriving while the active goal is
already over budget (e.g. after resuming an exhausted goal) blocks the
goal before the turn is marked goal-driven, so turnsUsed no longer
drifts, no spurious goal_continued telemetry fires, and the prompt runs
as an ordinary turn with the blocked-goal note injected.

This deliberately diverges from agent-core v1, which hard-stops with
zero grace and answers a prompt on an exhausted goal with a synthetic
model-less turn: budget overshoot is now bounded at one closing step in
exchange for consumed tool results and a user-facing wrap-up message.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants